using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Fuse8.DomainFramework.Common;

namespace Fuse8.DomainFramework.DataAccess.ExpressionSlicers
{
    /// <summary>
    /// Default implementation of <see cref="ISlicerService"/>
    /// </summary>
    public class DefaultSlicerService : ISlicerService
    {
        readonly Dictionary<Type, List<LambdaExpression>> registry = new Dictionary<Type, List<LambdaExpression>>();
        readonly Dictionary<Type, LambdaExpression> cache = new Dictionary<Type, LambdaExpression>();
        readonly Dictionary<Type, List<Func<LambdaExpression>>> asyncRegistry = new Dictionary<Type, List<Func<LambdaExpression>>>();
        private object locker = new object();

        /// <summary>
        /// Adds expression for specified and inherited types
        /// </summary>
        /// <param name="slicer"></param>
        /// <typeparam name="T"></typeparam>
        public void Add<T>(Expression<Func<T, bool>> slicer)
        {
            lock (locker)
            {
                var key = typeof(T);
                if (!registry.ContainsKey(key))
                {
                    registry.Add(key, new List<LambdaExpression>());
                }

                registry[key].Add(slicer);
            }
        }

        /// <summary>
        /// Adds not function to get expression for specified and inherited types
        /// </summary>
        /// <param name="asyncSlicer"></param>
        /// <typeparam name="T"></typeparam>
        public void AddDynamic<T>(Func<Expression<Func<T, bool>>> asyncSlicer)
        {
            lock (locker)
            {
                var key = typeof(T);
                if (!asyncRegistry.ContainsKey(key))
                {
                    asyncRegistry.Add(key, new List<Func<LambdaExpression>>());
                }

                asyncRegistry[key].Add(asyncSlicer);
            } 
        }

        /// <summary>
        /// Get resulting expression
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public LambdaExpression ResolveForType(Type type)
        {
            if (registry.Count == 0 && asyncRegistry.Count == 0)
            {
                return null;
            }
            var baseTypes = GetBaseTypes(type).Concat(type.GetInterfaces());
            if (!cache.ContainsKey(type))
            {
                LambdaExpression constExp = null;
                foreach (Type baseType in baseTypes)
                {
                    if (registry.ContainsKey(baseType))
                    {
                        foreach (var exp in registry[baseType])
                        {
                            constExp = And(constExp, exp, type);
                        }
                    }
                }
                cache[type] = constExp;
            }

            LambdaExpression result = cache[type];

            foreach (Type baseType in baseTypes)
            {
                if (asyncRegistry.ContainsKey(baseType))
                {
                    foreach (var func in asyncRegistry[baseType])
                    {
                        result = And(result, func.Invoke(), type);
                    }
                }
            }

            return result;
        }

        private IEnumerable<Type> GetBaseTypes(Type type)
        {
            yield return type;
            var t = type.BaseType;
            while (t != null)
            {
                yield return t;
                t = t.BaseType;
            }
        }

        private LambdaExpression And(Expression result, Expression slicer, Type entityType)
        {
            var slicer1 = AdaptParameterType(slicer, entityType);
            if (result == null)
            {
                return slicer1;
            }

            return result.And(slicer1);
        }

        private LambdaExpression AdaptParameterType(Expression slicer, Type entityType)
        {
            var tempExp = slicer as LambdaExpression;

            if (tempExp.Parameters.Count > 0)
            {
                var parameter = Expression.Parameter(entityType, tempExp.Parameters[0].Name);
                var res = new ExpTypeChanger(parameter).Visit(tempExp.Body);
                return Expression.Lambda(res, parameter);
            }

            return (LambdaExpression)slicer;
        }

        private class ExpTypeChanger : ExpressionVisitor
        {
            private readonly ParameterExpression _parameter;

            public ExpTypeChanger(ParameterExpression parameter)
            {
                _parameter = parameter;
            }            

            protected override Expression VisitParameter(ParameterExpression node)
            {
                return _parameter;
            }
        }
    }
}